#define STANDARD_BLOCK_SIZE (512)
#define ERROR_STR_BUF_LEN 0x100

#include "config.h"

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"

#include <fuse.h>

#pragma GCC diagnostic pop

#include <unistd.h>
#include <limits.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/statvfs.h>
#include <sys/xattr.h>

#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <limits>
#include <queue>

#include "fuse-vm.h"
#include "types.h"
#include "fuseVMData.h"
#include "util.h"

extern struct vmfuse_param param;
using namespace std;

FuseVMData *initFuseVM(const char *program)
{
    FuseVMData *data = NULL;
    // int err;
    try
    {
        // current working directory
        char *cwd = (char *)malloc(PATH_MAX + 1);
        if (cwd == NULL)
        {
            throw std::bad_alloc();
        }
        if (getcwd(cwd, PATH_MAX) == NULL)
        {
            perror(NULL);
            free(cwd);
            return data;
        }
        data = new FuseVMData(cwd);
        free(cwd);
        if (data == NULL)
        {
            throw std::bad_alloc();
        }
        try
        {
            data->build_tree();
        }
        catch (...)
        {
            delete data;
            throw;
        }
    }
    catch (std::bad_alloc &)
    {
        syslog(LOG_ERR, "no enough memory");
        fprintf(stderr, "%s: no enough memory\n", program);
        return NULL;
    }
    catch (const std::exception &e)
    {
        syslog(LOG_ERR, "error opening target file: %s", e.what());
        fprintf(stderr, "%s: unable to open target file: %s\n", program, e.what());
        return NULL;
    }
    // printf("%s: init data address [%p]\n", __FUNCTION__, data);
    return data;
}

void *fusevm_init(struct fuse_conn_info *conn)
{
    (void)conn;
    FuseVMData *data = (FuseVMData *)fuse_get_context()->private_data;
    syslog(LOG_INFO, "Mounting file system (cwd=%s), data address [%p]", data->m_cwd.c_str(), (void *)data);
    return data;
}

inline FuseVMData *get_data()
{
    return (FuseVMData *)fuse_get_context()->private_data;
}

void fusevm_destroy(void *data)
{
    FuseVMData *d = (FuseVMData *)data;
    delete d;
    if(param.memoryFlag == false && strlen(param.storeDir) > 0)
    {
        std::string cmd = "rm -rf ";
        cmd.append(param.storeDir).append("/*");
        system(cmd.c_str());
    }
    syslog(LOG_INFO, "File system unmounted");
    return;
}

FileNode *get_file_node(const char *fname)
{
    return get_data()->find(fname);
}

int fusevm_getattr(const char *path, struct stat *stbuf)
{
    memset(stbuf, 0, sizeof(struct stat));
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    if (node->is_dir())
    {
        stbuf->st_nlink = 2 + node->num_dir_links();
    }
    else
    {
        stbuf->st_nlink = static_cast<nlink_t>(node->use_count());
    }
    stbuf->st_mode = node->mode();
    stbuf->st_blksize = STANDARD_BLOCK_SIZE;
    // Interpreting pointer as ulong without loss of data is valid for LP32, ILP32, LP64 and ILP64 data models.
    // The only well-known data model that break this is LLP64 (Win64 API).
    stbuf->st_ino = reinterpret_cast<unsigned long>(node->data());
    stbuf->st_blocks = static_cast<blkcnt_t>((node->size() + STANDARD_BLOCK_SIZE - 1) / STANDARD_BLOCK_SIZE);
    stbuf->st_size = static_cast<off_t>(node->size());
#if __APPLE__
    stbuf->st_atimespec = node->atime();
    stbuf->st_mtimespec = node->mtime();
    stbuf->st_ctimespec = node->ctime();
#else  // __APPLE__
    stbuf->st_atim = node->atime();
    stbuf->st_mtim = node->mtime();
    stbuf->st_ctim = node->ctime();
#endif // __APPLE__
    stbuf->st_uid = node->uid();
    stbuf->st_gid = node->gid();
    stbuf->st_rdev = node->device();
    return 0;
}

int fusevm_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
{
    (void)offset;
    (void)fi;

    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    filler(buf, ".", NULL, 0);
    filler(buf, "..", NULL, 0);
    for (nodelist_t::const_iterator i = node->childs.begin(); i != node->childs.end(); ++i)
    {
        filler(buf, (*i)->name, NULL, 0);
    }
    return 0;
}

int fusevm_statfs(const char *path, struct statvfs *buf)
{
    (void)path;
    buf->f_bsize = BLOCKSIZE;
    buf->f_frsize = BLOCKSIZE;
    buf->f_blocks = MAX_BLOCKS;
    buf->f_bfree = MAX_BLOCKS;
    buf->f_bavail = MAX_BLOCKS;
    buf->f_files = get_data()->numFiles();
    buf->f_ffree = MAX_INODE;
    buf->f_favail = MAX_INODE;
    buf->f_fsid = FS_ID;
    buf->f_namemax = MAX_NAME;
    return 0;
}

int fusevm_open(const char *path, struct fuse_file_info *fi)
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    if (node->is_dir())
    {
        return -EISDIR;
    }
    fi->fh = (uint64_t)node;
    try
    {
        return node->open();
    }
    catch (std::bad_alloc &)
    {
        return -ENOMEM;
    }
    catch (std::exception &)
    {
        return -EIO;
    }
}

int fusevm_create(const char *path, mode_t mode, struct fuse_file_info *fi)
{
    if (*path == '\0')
    {
        return -EACCES;
    }
    FileNode *node = get_file_node(path + 1);
    if (node != NULL)
    {
        return -EEXIST;
    }
    node = FileNode::createFile(path + 1, fuse_get_context()->uid, fuse_get_context()->gid, mode);
    if (node == NULL)
    {
        return -ENOMEM;
    }
    get_data()->insertNode(node);
    fi->fh = (uint64_t)node;
    return node->open();
}

int fusevm_mknod(const char *path, mode_t mode, dev_t dev)
{
    if (*path == '\0')
    {
        return -EACCES;
    }
    FileNode *node = get_file_node(path + 1);
    if (node != NULL)
    {
        return -EEXIST;
    }
    node = FileNode::createFile(path + 1, fuse_get_context()->uid, fuse_get_context()->gid, mode, dev);
    if (node == NULL)
    {
        return -ENOMEM;
    }
    get_data()->insertNode(node);
    return 0;
}

int fusevm_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
    (void)path;

    if (offset < 0)
        return -EINVAL;

    //syslog(LOG_ERR, "[%ld]start read: path=%s offset=%ld size=%ld\n",usecTime(), path, offset, size);
    return ((FileNode *)fi->fh)->read(buf, size, static_cast<size_t>(offset));
}

int fusevm_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
    (void)path;

    if (offset < 0)
        return -EINVAL;
    //syslog(LOG_ERR, "[%ld]start write: path=%s offset=%ld size=%ld\n",usecTime(), path, offset, size);
    return ((FileNode *)fi->fh)->write(buf, size, static_cast<size_t>(offset));
}

int fusevm_release(const char *path, struct fuse_file_info *fi)
{
    (void)path;
    return ((FileNode *)fi->fh)->close();
}

int fusevm_ftruncate(const char *path, off_t offset, struct fuse_file_info *fi)
{
    (void)path;

    if (offset < 0)
        return -EINVAL;
    return -((FileNode *)fi->fh)->truncate(static_cast<size_t>(offset));
}

int fusevm_truncate(const char *path, off_t offset)
{
    if (*path == '\0')
    {
        return -EACCES;
    }
    if (offset < 0)
        return -EINVAL;
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    if (node->is_dir())
    {
        return -EISDIR;
    }
    int res;
    if ((res = node->open()) != 0)
    {
        return res;
    }
    if ((res = node->truncate(static_cast<size_t>(offset))) != 0)
    {
        node->close();
        return -res;
    }
    return node->close();
}

int fusevm_unlink(const char *path)
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    if (node->is_dir())
    {
        return -EISDIR;
    }
    return -get_data()->removeNode(node);
}

int fusevm_rmdir(const char *path)
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    if (!node->is_dir())
    {
        return -ENOTDIR;
    }
    if (!node->childs.empty())
    {
        return -ENOTEMPTY;
    }
    return -get_data()->removeNode(node);
}

int fusevm_mkdir(const char *path, mode_t mode)
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = FileNode::createDir(path + 1, 2, fuse_get_context()->uid, fuse_get_context()->gid, mode);
    if (node == NULL)
    {
        return -ENOMEM;
    }
    get_data()->insertNode(node);
    return 0;
}

int fusevm_rename(const char *path, const char *new_path)
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    if (*new_path == '\0')
    {
        return -EINVAL;
    }
    FileNode *new_node = get_file_node(new_path + 1);
    if (new_node != NULL)
    {
        int res = get_data()->removeNode(new_node);
        if (res != 0)
        {
            return -res;
        }
    }

    size_t len = strlen(new_path);
    size_t oldLen = strlen(path + 1) + 1;
    std::string new_name;
    if (!node->is_dir())
    {
        --len;
        --oldLen;
    }
    new_name.reserve(len + ((node->is_dir()) ? 1 : 0));
    new_name.append(new_path + 1);
    if (node->is_dir())
    {
        new_name.push_back('/');
    }

    try
    {
        // Renaming directory and its content recursively
        if (node->is_dir())
        {
            queue<FileNode *> q;
            q.push(node);
            while (!q.empty())
            {
                FileNode *n = q.front();
                q.pop();
                for (nodelist_t::const_iterator i = n->childs.begin(); i != n->childs.end(); ++i)
                {
                    FileNode *nn = *i;
                    q.push(nn);
                    char *name = (char *)malloc(len + nn->full_name.size() - oldLen + (nn->is_dir() ? 2 : 1));
                    if (name == NULL)
                    {
                        // TODO: check that we are have enough memory before entering this loop
                        return -ENOMEM;
                    }
                    strcpy(name, new_name.c_str());
                    strcpy(name + len, nn->full_name.c_str() + oldLen);
                    if (nn->is_dir())
                    {
                        strcat(name, "/");
                    }
                    // changing child list may cause loop iterator corruption
                    get_data()->renameNode(nn, name, false);

                    free(name);
                }
            }
        }
        get_data()->renameNode(node, new_name.c_str(), true);

        return 0;
    }
    catch (...)
    {
        return -EIO;
    }
}

int fusevm_utimens(const char *path, const struct timespec tv[2])
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    node->setTimes(tv[0], tv[1]);
    return 0;
}

#if (__APPLE__)
int fusevm_setxattr(const char *, const char *, const char *, size_t, int, uint32_t)
{
#else
int fusevm_setxattr(const char *, const char *, const char *, size_t, int)
{
#endif
    return 0;
}

#if (__APPLE__)
int fusevm_getxattr(const char *, const char *, char *, size_t, uint32_t)
{
#else
int fusevm_getxattr(const char *, const char *, char *, size_t)
{
#endif
    return 0;
}

int fusevm_listxattr(const char *, char *, size_t)
{
    return 0;
}

int fusevm_removexattr(const char *, const char *)
{
    return 0;
}

int fusevm_chmod(const char *path, mode_t mode)
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    node->chmod(mode);
    return 0;
}

int fusevm_chown(const char *path, uid_t uid, gid_t gid)
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    if (uid != (uid_t)-1)
    {
        node->setUid(uid);
    }
    if (gid != (gid_t)-1)
    {
        node->setGid(gid);
    }
    return 0;
}

int fusevm_flush(const char *, struct fuse_file_info *)
{
    return 0;
}

int fusevm_fsync(const char *, int, struct fuse_file_info *)
{
    return 0;
}

int fusevm_fsyncdir(const char *, int, struct fuse_file_info *)
{
    return 0;
}

int fusevm_opendir(const char *, struct fuse_file_info *)
{
    return 0;
}

int fusevm_releasedir(const char *, struct fuse_file_info *)
{
    return 0;
}

int fusevm_access(const char *, int)
{
    return 0;
}

int fusevm_readlink(const char *path, char *buf, size_t size)
{
    if (*path == '\0')
    {
        return -ENOENT;
    }
    FileNode *node = get_file_node(path + 1);
    if (node == NULL)
    {
        return -ENOENT;
    }
    if (!S_ISLNK(node->mode()))
    {
        return -EINVAL;
    }
    int res;
    if ((res = node->open()) != 0)
    {
        if (res == -EMFILE)
        {
            res = -ENOMEM;
        }
        return res;
    }
    int count = node->read(buf, size - 1, 0);
    buf[count] = '\0';
    node->close();
    return 0;
}

int fusevm_symlink(const char *dest, const char *path)
{
    if (*path == '\0')
    {
        return -EACCES;
    }
    FileNode *node = get_file_node(path + 1);
    if (node != NULL)
    {
        return -EEXIST;
    }
    node = FileNode::createSymlink(path + 1);
    if (node == NULL)
    {
        return -ENOMEM;
    }
    get_data()->insertNode(node);

    int res;
    if ((res = node->open()) != 0)
    {
        if (res == -EMFILE)
        {
            res = -ENOMEM;
        }
        return res;
    }
    res = node->write(dest, strlen(dest), 0);
    node->close();
    return (res < 0) ? -ENOMEM : 0;
}